Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.09% covered (success)
96.09%
123 / 128
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieServiceProvider
96.09% covered (success)
96.09%
123 / 128
50.00% covered (danger)
50.00%
2 / 4
26
0.00% covered (danger)
0.00%
0 / 1
 autoTagHashmapActions
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 boot
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 register
94.37% covered (success)
94.37%
67 / 71
0.00% covered (danger)
0.00%
0 / 1
14.04
 sanitizeConfig
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2namespace Apie\LaravelApie;
3
4use Apie\AiInstructor\AiInstructorServiceProvider;
5use Apie\ApieCommonPlugin\ApieCommonPluginServiceProvider;
6use Apie\CmsApiDropdownOption\CmsDropdownServiceProvider;
7use Apie\Common\CommonServiceProvider;
8use Apie\Common\ContextBuilders\FrameworkContextBuilder;
9use Apie\Common\Interfaces\BoundedContextSelection;
10use Apie\Common\Interfaces\DashboardContentFactoryInterface;
11use Apie\Common\Wrappers\BoundedContextHashmapFactory;
12use Apie\Common\Wrappers\ConsoleCommandFactory as CommonConsoleCommandFactory;
13use Apie\Console\ConsoleServiceProvider;
14use Apie\Core\CoreServiceProvider;
15use Apie\Core\Session\CsrfTokenProvider;
16use Apie\DoctrineEntityConverter\DoctrineEntityConverterProvider;
17use Apie\DoctrineEntityDatalayer\Commands\ApieUpdateIdfCommand;
18use Apie\DoctrineEntityDatalayer\DoctrineEntityDatalayerServiceProvider;
19use Apie\DoctrineEntityDatalayer\EntityReindexer;
20use Apie\DoctrineEntityDatalayer\IndexStrategy\BackgroundIndexStrategy;
21use Apie\DoctrineEntityDatalayer\IndexStrategy\DirectIndexStrategy;
22use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexAfterResponseIsSentStrategy;
23use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexStrategyInterface;
24use Apie\Faker\FakerServiceProvider;
25use Apie\HtmlBuilders\ErrorHandler\CmsErrorRenderer;
26use Apie\HtmlBuilders\HtmlBuilderServiceProvider;
27use Apie\LaravelApie\Config\LaravelConfiguration;
28use Apie\LaravelApie\ContextBuilders\CsrfTokenContextBuilder;
29use Apie\LaravelApie\ContextBuilders\RegisterBoundedContextActionContextBuilder;
30use Apie\LaravelApie\ContextBuilders\SessionContextBuilder;
31use Apie\LaravelApie\ErrorHandler\ApieErrorRenderer;
32use Apie\LaravelApie\ErrorHandler\Handler;
33use Apie\LaravelApie\Providers\CmsServiceProvider;
34use Apie\LaravelApie\Providers\SecurityServiceProvider;
35use Apie\LaravelApie\Wrappers\Cms\DashboardContentFactory;
36use Apie\LaravelApie\Wrappers\Core\BoundedContextSelected;
37use Apie\LaravelApie\Wrappers\Queue\BackgroundProcessPersistListener;
38use Apie\Maker\MakerServiceProvider;
39use Apie\RestApi\RestApiServiceProvider;
40use Apie\SchemaGenerator\SchemaGeneratorServiceProvider;
41use Apie\Serializer\SerializerServiceProvider;
42use Apie\ServiceProviderGenerator\TagMap;
43use Apie\TypescriptClientBuilder\TypescriptClientBuilderServiceProvider;
44use Illuminate\Config\Repository;
45use Illuminate\Contracts\Debug\ExceptionHandler;
46use Illuminate\Contracts\Events\Dispatcher;
47use Illuminate\Support\ServiceProvider;
48use Psr\EventDispatcher\EventDispatcherInterface;
49use Psr\Http\Message\ServerRequestInterface;
50use Symfony\Component\Config\ConfigCache;
51use Symfony\Component\Config\Definition\Processor;
52use Symfony\Component\Config\Resource\ReflectionClassResource;
53use Symfony\Component\Console\Application;
54use Symfony\Component\EventDispatcher\EventDispatcher;
55use Symfony\Component\Lock\LockFactory;
56
57class ApieServiceProvider extends ServiceProvider
58{
59    /**
60     * @var array<string, array<int, class-string<ServiceProvider>>> $dependencies
61     */
62    private array $dependencies = [
63        'enable_ai_instructor' => [
64            AiInstructorServiceProvider::class,
65        ],
66        'enable_common_plugin' => [
67            ApieCommonPluginServiceProvider::class,
68        ],
69        'enable_cms' => [
70            CommonServiceProvider::class,
71            HtmlBuilderServiceProvider::class, // it's important that this loads before CmsServiceProvider!!!
72            CmsServiceProvider::class,
73            SerializerServiceProvider::class,
74        ],
75        'enable_cms_dropdown' => [
76            CommonServiceProvider::class,
77            CmsDropdownServiceProvider::class,
78        ],
79        'enable_core' => [
80            CoreServiceProvider::class,
81        ],
82        'enable_console' => [
83            CommonServiceProvider::class,
84            ConsoleServiceProvider::class,
85            SerializerServiceProvider::class,
86        ],
87        'enable_doctrine_entity_converter' => [
88            CoreServiceProvider::class,
89            DoctrineEntityConverterProvider::class,
90        ],
91        'enable_doctrine_entity_datalayer' => [
92            CoreServiceProvider::class,
93            DoctrineEntityConverterProvider::class,
94            DoctrineEntityDatalayerServiceProvider::class,
95        ],
96        'enable_security' => [
97            CommonServiceProvider::class,
98            SerializerServiceProvider::class,
99            SecurityServiceProvider::class,
100        ],
101        'enable_rest_api' => [
102            CommonServiceProvider::class,
103            RestApiServiceProvider::class,
104            SchemaGeneratorServiceProvider::class,
105            SerializerServiceProvider::class,
106        ],
107        'enable_faker' => [
108            FakerServiceProvider::class,
109        ],
110        'enable_maker' => [
111            MakerServiceProvider::class,
112        ],
113        'enable_typescript_client_builder' => [
114            TypescriptClientBuilderServiceProvider::class,
115        ],
116    ];
117
118    private function autoTagHashmapActions(): void
119    {
120        $boundedContextConfig = config('apie.bounded_contexts');
121        $scanBoundedContextConfig = config('apie.scan_bounded_contexts');
122        $factory = new BoundedContextHashmapFactory(
123            $boundedContextConfig ?? [],
124            $scanBoundedContextConfig ?? [],
125            new EventDispatcher(),
126        );
127        $hashmap = $factory->create();
128        foreach ($hashmap as $boundedContext) {
129            foreach ($boundedContext->actions as $action) {
130                $class = $action->getDeclaringClass();
131                if (!$class->isInstantiable()) {
132                    continue;
133                }
134                $className = $class->name;
135                TagMap::register(
136                    $this->app,
137                    $className,
138                    ['apie.context']
139                );
140            }
141        }
142    }
143
144    public function boot(): void
145    {
146        $this->autoTagHashmapActions();
147        $this->loadViewsFrom(__DIR__ . '/../templates', 'apie');
148        $this->loadRoutesFrom(__DIR__.'/../resources/routes.php');
149        TagMap::registerEvents($this->app);
150
151        if ($this->app->runningInConsole()) {
152            $commands = [];
153            $commands[] = ApieUpdateIdfCommand::class;
154            // for some reason these are not called in integration tests without re-registering them
155            foreach (TagMap::getServiceIdsWithTag($this->app, 'console.command') as $taggedCommand) {
156                $serviceId = 'apie.console.tagged.' . $taggedCommand;
157                $this->app->singleton($serviceId, function () use ($taggedCommand) {
158                    return $this->app->get($taggedCommand);
159                });
160                $commands[] = $serviceId;
161            }
162            /** @var CommonConsoleCommandFactory $factory */
163            $factory = $this->app->get('apie.console.factory');
164            foreach ($factory->create($this->app->get(Application::class)) as $command) {
165                $serviceId = 'apie.console.registered.' . $command->getName();
166                $this->app->instance($serviceId, $command);
167                $commands[] = $serviceId;
168            }
169            $this->commands($commands);
170        }
171    }
172
173    public function register()
174    {
175        $this->mergeConfigFrom(__DIR__ . '/../resources/apie.php', 'apie');
176
177        $this->app->bind(FrameworkContextBuilder::class, function () {
178            return new FrameworkContextBuilder('laravel');
179        });
180        TagMap::register($this->app, FrameworkContextBuilder::class, ['apie.core.context_builder']);
181
182        // add PSR-14 support if needed:
183        if (!$this->app->bound(EventDispatcherInterface::class)) {
184            $this->app->bind(EventDispatcherInterface::class, function () {
185                return new class($this->app->make(Dispatcher::class)) implements EventDispatcherInterface {
186                    public function __construct(private readonly Dispatcher $dispatcher)
187                    {
188                    }
189
190                    public function dispatch(object $event): object
191                    {
192                        $this->dispatcher->dispatch($event);
193                        return $event;
194                    }
195                };
196            });
197        }
198
199        // fix for https://github.com/laravel/framework/issues/30415
200        $this->app->extend(
201            ServerRequestInterface::class,
202            function (ServerRequestInterface $psrRequest) {
203                $route = $this->app->make('request')->route();
204                if ($route) {
205                    $parameters = $route->parameters();
206                    foreach ($parameters as $key => $value) {
207                        $psrRequest = $psrRequest->withAttribute($key, $value);
208                    }
209                }
210                return $psrRequest;
211            }
212        );
213
214        $this->app->bind(IndexStrategyInterface::class, function () {
215            $config = config();
216            if ($config->get('apie.enable_doctrine_entity_datalayer')) {
217                $type = $config->get('apie.doctrine.indexing.type', 'direct');
218                return match ($type) {
219                    'direct' => new DirectIndexStrategy($this->app->get(EntityReindexer::class)),
220                    'late' => new IndexAfterResponseIsSentStrategy($this->app->get(EntityReindexer::class)),
221                    'background' => new BackgroundIndexStrategy(),
222                    default => $this->app->get(config('apie.doctrine.indexing.service', DirectIndexStrategy::class)),
223                };
224            }
225
226            return new DirectIndexStrategy($this->app->get(EntityReindexer::class));
227        });
228
229        $this->app->bind(ApieErrorRenderer::class, function () {
230            return new ApieErrorRenderer(
231                $this->app->bound(CmsErrorRenderer::class) ? $this->app->make(CmsErrorRenderer::class) : null,
232                $this->app->make(\Apie\Common\ErrorHandler\ApiErrorRenderer::class),
233                config('apie.cms.base_url')
234            );
235        });
236
237        $this->app->extend(ExceptionHandler::class, function (ExceptionHandler $service) {
238            return new Handler($this->app, $service);
239        });
240
241        $this->app->bind(LockFactory::class, function () {
242            $config = config('apie.lock_store');
243            return new LockFactory($this->app->get($config));
244        });
245        
246        $this->app->bind(DashboardContentFactoryInterface::class, DashboardContentFactory::class);
247        $this->app->bind(BoundedContextSelection::class, BoundedContextSelected::class);
248
249        $alreadyRegistered = [];
250        foreach ($this->dependencies as $configKey => $dependencies) {
251            if (config('apie.' . $configKey, false)) {
252                foreach ($dependencies as $dependency) {
253                    if (!isset($alreadyRegistered[$dependency])) {
254                        $alreadyRegistered[$dependency] = $dependency;
255                        $this->app->register($dependency);
256                    }
257                }
258            }
259        }
260        //$this->app->bind(CsrfTokenProvider::class, CsrfTokenContextBuilder::class);
261        TagMap::register($this->app, CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
262        $this->app->tag(CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
263
264        // this has to be added after CsrfTokenContextBuilder!
265        $this->app->bind(SessionContextBuilder::class);
266        TagMap::register($this->app, SessionContextBuilder::class, ['apie.core.context_builder']);
267        $this->app->tag(SessionContextBuilder::class, ['apie.core.context_builder']);
268
269        TagMap::register($this->app, RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
270        $this->app->tag(RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
271        $this->app->extend('config', function (Repository $config) {
272            $this->sanitizeConfig($config);
273        
274            return $config;
275        });
276
277        TagMap::register($this->app, BackgroundProcessPersistListener::class, ['kernel.event_subscriber']);
278    }
279
280    private function sanitizeConfig(Repository $config): void
281    {
282        $rawConfig = $config->get('apie');
283        $path = storage_path('framework/cache/apie-config' . md5(json_encode($rawConfig)) . '.php');
284        $resources = [
285            new ReflectionClassResource(new \ReflectionClass(LaravelConfiguration::class)),
286            new ReflectionClassResource(new \ReflectionClass(static::class)),
287        ];
288        $configCache = new ConfigCache($path, true);
289        if ($configCache->isFresh()) {
290            $processedConfig = require $path;
291        } else {
292            $configuration = new LaravelConfiguration();
293
294            $processor = new Processor();
295
296            $processedConfig = $processor->processConfiguration($configuration, ['apie' => $rawConfig]);
297
298            if (!isset($processedConfig['scan_bounded_contexts'])) {
299                $processedConfig['scan_bounded_contexts'] = [];
300            }
301            if (empty($processedConfig['storage'])) {
302                $processedConfig['storage'] = null;
303            }
304            $code = '<?php' . PHP_EOL . 'return ' . var_export($processedConfig, true) . ';';
305            $configCache->write($code, $resources);
306        }
307
308        $config->set('apie', $processedConfig);
309    }
310}